package org.melanesia.sql;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Logger;

import org.melanesia.beans.Mapper;
import org.melanesia.beans.PropertyMapper;
import org.melanesia.converters.JavaToJavaConverter;
import org.melanesia.converters.JavaToJdbcConverter;
import org.melanesia.exceptions.UnsupportedClassException;
import org.melanesia.sql.exceptions.SQLExecutionException;
import org.melanesia.sql.exceptions.SQLMappingException;


public class Call extends AbstractSQL {

	private final static Logger logger = Logger.getLogger(Call.class.getName());
	
    private PropertyMapper propertyMapper = new PropertyMapper();
    
    private Connection connection;
    private String sql;
    private CallMetaData callMetaData = new CallMetaData();
    
    Call(Connection connection, Mapper mapper, String sql) {
        super(mapper);
        this.connection = connection;
        this.sql = sql;
    }
    
    public Call setInputParameter(String name, Object value) {
        callMetaData.setInputParameter(name, value);
        return this;
    }

    public Call setInputParameter(String name, Object value, JavaToJdbcConverter converter) {
        callMetaData.setInputParameter(name, value, converter);
        return this;
    }
    
    public Call registerOutputParameter(String name, Class<?> parameterClass) {
        callMetaData.registerOutputParameter(name, parameterClass);
        return this;
    }
    
    public Call registerOutputParameter(String name, Class<?> parameterClass, JavaToJavaConverter converter) {
        callMetaData.registerOutputParameter(name, parameterClass, converter);
        return this;
    }
    
    public Call setPropertyMapper(PropertyMapper propertyMapper) {
        this.propertyMapper = propertyMapper;
        return this;
    }
    
    public <T> T execute(Class<T> returnedBeanClass) {
        
        CallableStatement stmt = null;
        ResultSet rs = null;
        
        try {
        	
        	String sqlId = String.valueOf(sql.hashCode());
        	logger.info("[CALL:" + sqlId + "]: Executing query\n" + sql);
        	
            stmt = connection.prepareCall(sql);
            
            for (String parameterName : callMetaData.getInputParameterNames()) {
                InputParameter parameter = callMetaData.getInputParameter(parameterName);
                setInputParameter(stmt, parameter);
                
                logger.info("[QUERY:" + sqlId + "]: Bound parameter " + parameter.getName() + " = " + parameter.getValue());
            }
            
            for (String parameterName : callMetaData.getOutputParameterNames()) {
                OutputParameter parameter = callMetaData.getOutputParameter(parameterName);
                registerOutputParameter(stmt, parameter);
            }
            
            long sqlTimeStart = System.currentTimeMillis();
            rs = stmt.executeQuery();
            long sqlTime = System.currentTimeMillis() - sqlTimeStart;
            
            T bean = null;
            
            long mappingTimeStart = System.currentTimeMillis();
            if (rs.next()) {
            	// FIXME: Will this work when Map.class is passed?
                bean = returnedBeanClass.newInstance();
             
                for (String parameterName : callMetaData.getOutputParameterNames()) {
                    Object parameterValue = rs.getObject(parameterName);
                    propertyMapper.setProperty(bean, parameterName, parameterValue, mapper);
                }
            }
            
            long mappingTime = System.currentTimeMillis() - mappingTimeStart;
            
            logger.info("[CALL:" + sqlId + "]: DB: " + sqlTime + "ms, FETCH&MAPPING: " + mappingTime + "ms");
            
            return bean;
            
        } catch (SQLException ex) {
            throw new SQLExecutionException(ex);
        } catch (InstantiationException ex) {
            throw new SQLMappingException(ex);
        } catch (IllegalAccessException ex) {
            throw new SQLMappingException(ex);
        } finally {
            try {rs.close();} catch (Throwable t) {}
            try {stmt.close();} catch (Throwable t) {}
        }
    }
    
    private void registerOutputParameter(CallableStatement stmt, OutputParameter parameter) throws SQLException {
    	String parameterName = parameter.getName();
        Class<?> parameterClass = parameter.getParameterClass();
        
    	JavaToJdbcConverter converter = mapper.findJavaToJdbcConverter(parameterClass);
        
        if (converter == null) {
        	// There is no converter, so no way to determine JDBC Type
            throw new UnsupportedClassException(parameterClass);
        }
        
        int jdbcType = converter.getJdbcType();        
        stmt.registerOutParameter(parameterName, jdbcType);
    }
}
